#!/usr/bin/perl

use strict;
use warnings;


my $RULES = "/etc/udev/rules.d/70-persistent-net.rules";


#-----------------------------------------------------------------------------#
# Sanity check
#-----------------------------------------------------------------------------#
die "$RULES does not exist to fix\n" unless -f $RULES;


#-----------------------------------------------------------------------------#
# Parse existing /etc/udev/rules.d/70-persistent-net.rules
#-----------------------------------------------------------------------------#

open RULES, $RULES
    or die "Unable to open $RULES: $!";

my @rules;
while (<RULES>) {
    # Rules may cross multiple lines when lines ends in \
    while (/\\$/ && (my $extra = <RULES>)) {
	$_ .= $extra;
    }

    # Skip lines that are empty, have only whitespace and/or comments
    if (/^\s*(\#.*)?$/) {
	push @rules, [ $_ ];
	next;
    }

    # Parse rule elements
    my $rule = [];
    while (/(?:([\s,]*)((\S*?)\s*(==|\+=|!=|:=|=)\s*\"([^\"]*)\")|(.+))/gs) {
	if (defined $1) {
	    push @{$rule}, $1 if length $1;
	    push @{$rule}, {	
		'text' => $2,
		'key' => $3,
		'op' => $4,
		'val' => $5
	    };
	} else {
	    push @{$rule}, $6;
	}
    }

    push @rules, $rule;
}

close RULES
    or warn "Error while closing $RULES: $!";


#-----------------------------------------------------------------------------#
# Fix rule entries
#-----------------------------------------------------------------------------#

RULE: foreach my $rule (@rules) {
    my $has_rule = 0;
    my $has_match = 0;
    my $has_type = 0;
    my $name_idx = -1;
    my $name;
    my $address;

    MATCH: foreach my $idx (0..$#{$rule}) {
	my $match = $rule->[$idx];
	next unless ref $match;
	$has_rule = 1;

	if ($match->{key} =~ /^ATTRS?{type}$/) {
	    # Rule matches on interface type
	    $has_type = 1;
	} elsif ($match->{key} =~ /^ATTRS?{address}$/) {
	    # Rule matches on MAC address
	    $has_match = 1;
	    $address = $match->{val};
	} elsif ($match->{key} =~ /^ATTRS?{[^}]*}|ENV{[^}]*}|KERNELS$/) {
	    # Rule matches on a possibly unique attribute
	    $has_match = 1;
	} elsif ($match->{key} =~ /^NAME$/ && $match->{op} =~ /^:?=$/) {
	    # Name setting
	    $name_idx = $idx;
	    $name = $match->{val};
	}
    }

    next unless $has_rule;

    # Comment out lines that don't match a device by any potentially unique
    # property, since they'll match everything
    unless ($has_match) {
	unshift @{$rule}, "#removed by upgrade, will match any device\n# ";
	$name_idx++;
    }

    # Add a match on type for any rule missing that to avoid matching
    # "master" devices
    unless ($has_type) {
	my $type;

	# Make sure that the device currently has the "1" (Ethernet) type
	if (-f "/sys/class/net/$name/type"
	    && `cat /sys/class/net/$name/type` =~ /^1\s*$/) {
	    # It does
	    $type = "1";
	} elsif (defined $address) {
	    # It doesn't, it's possible that we've already renamed it to
	    # something else and a device exists with the right MAC address
	    # and a different name
	    opendir NET, "/sys/class/net"
		or die "Unable to open /sys/class/net: $!";

	    my $found = 0;
	    while (my $dev = readdir(NET)) {
		next if $dev =~ /^\./;
		next unless (-f "/sys/class/net/$dev/address"
			     && `cat /sys/class/net/$dev/address` =~ /^$address\s*$/i);
		$found = 1;

		next unless (-f "/sys/class/net/$dev/type"
			     && `cat /sys/class/net/$dev/type` =~ /^1\s*$/);

		# Found a device with a matching address and the right type
		$type = "1";
	    }

	    closedir NET
		or die "Error while closing /sys/class/net: $!";

	    # No device with the matching MAC address found (unplugged?)
	    # stick type in anyway
	    $type = "1" unless $found;
	}
	
	# Append a rule to match the type.  If we couldn't find an Ethernet
	# device with the set name, or a different name but the right address,
	# then we don't attempt to change the file and instead leave a comment
	# to highlight a line that might be a problem.
	if (defined $type) {
	    my $match = {
		'text' => "ATTR{type}==\"$type\"",
		'key' => 'ATTR{type}',
		'op' => '==',
		'val' => "$type"
	    };
	    splice(@{$rule}, $name_idx, 0, $match, ", ");
	    $name_idx += 2;
	} elsif ($has_match) {
	    unshift @{$rule}, "#following line may cause a device to become stuck as ${name}_rename\n#add ADDR{type}==\"...\" if that happens\n";
	    $name_idx++;
	}
    }
}


#-----------------------------------------------------------------------------#
# Write new /etc/udev/rules.d/70-persistent-net.rules
#-----------------------------------------------------------------------------#

open RULES, ">$RULES.new"
    or die "Unable to open $RULES.new: $!";

foreach my $rule (@rules) {
    my $line = "";
    foreach my $match (@{$rule}) {
	if (ref $match) {
	    $line .= $match->{text};
	} else {
	    $line .= $match;
	}
    }

    print RULES $line;
}

close RULES
    or warn "Error while closing $RULES: $!";
rename "$RULES.new", $RULES
    or die "Unable to replace $RULES: $!";
